Python Data Structures: NamedTuple vs. @dataclass

A deep dive into modern Python data structures. Learn the critical differences between NamedTuple and @dataclass to choose the right tool for your code.
Python
Coding
Best Practices
Author

Bulent Soykan

Published

February 15, 2026

The End of Boilerplate

For years, writing a class in Python just to hold data was tedious. You had to write an __init__ method, a __repr__ method to make it readable, and often an __eq__ method to compare instances. It was a lot of noise for very little signal.

Today, Python offers two powerful solutions to this problem: NamedTuple and @dataclass. Both allow you to create structured data types with type hints, but they serve fundamentally different purposes. The choice usually comes down to one question: Do you need to change the data after you create it?

Here is how to decide.

1. The Modern NamedTuple

The NamedTuple (from the typing module) is the type-hinted evolution of the older collections.namedtuple. It retains the memory efficiency of a standard tuple but enforces strict structure.

Think of a NamedTuple like a coordinate on a map or a row in a database. It is a snapshot of data that should not change.

Syntax

from typing import NamedTuple

class Card(NamedTuple):
    rank: str
    suit: str

# Usage
c1 = Card("A", "Spades")
print(c1.rank)  # Output: A

Key Traits

  • Immutable: This is the defining feature. Once you create c1, you cannot change it. c1.rank = "K" will raise an AttributeError.
  • It is a Tuple: Because it inherits from tuple, it behaves like one. You can iterate over it (for x in c1), access items by index (c1[0]), and unpack it (rank, suit = c1).
  • Lightweight: It uses significantly less memory than a standard class because it doesn’t need a __dict__ to store attributes per instance.

2. The @dataclass

Introduced in Python 3.7, the @dataclass decorator is the standard for creating classes that primarily store state. It automatically generates the boilerplate code (__init__, __repr__, etc.) for you, but unlike a tuple, it creates a full-fledged object.

Think of a @dataclass like a configuration object or a player entity in a game. It holds data that might need to evolve.

Syntax

from dataclasses import dataclass

@dataclass
class CardData:
    rank: str
    suit: str

# Usage
c2 = CardData("A", "Hearts")
c2.rank = "K"    # This works! The object is mutable.

Key Traits

  • Mutable: You can update fields freely after initialization.
  • It is a Class: It behaves like a standard object. You cannot index it (c2[0] fails) or unpack it without writing custom helper methods.
  • Rich Features: It supports complex inheritance, default values, and post-initialization processing (__post_init__) much better than NamedTuple.

Comparison: Which one to use?

Here is the breakdown of technical differences:

Feature NamedTuple @dataclass
Mutability Immutable (Read-only) Mutable (Read-write)
Type Hints Yes Yes
Memory Use Low (Tiny overhead) Normal (Class overhead)
Behavior Acts like a Tuple (x, y) Acts like an Object obj.x
Best For Coordinates, Return values Configs, State, Entities

The Decision Matrix

Choose NamedTuple if:

  1. Safety is a priority. You want to ensure the data is read-only and cannot be accidentally modified by another part of your code.
  2. You need tuple behavior. You want to unpack the data (x, y = point) or pass it to functions that expect sequences.
  3. Performance matters. You are processing millions of small objects and need to minimize memory footprint.

Choose @dataclass if:

  1. Data evolves. You are building something like a User profile where fields (like last_login or score) change over time.
  2. You need logic. You plan to add custom methods to the class (e.g., def is_valid(self):).
  3. You need inheritance. Dataclasses handle class hierarchies more gracefully than named tuples.

The Hybrid Approach: frozen=True

What if you want the features of a @dataclass (like nice inheritance) but the safety of a NamedTuple (immutability)?

You can “freeze” a dataclass:

@dataclass(frozen=True)
class ImmutablePoint:
    x: int
    y: int

p = ImmutablePoint(10, 20)
# p.x = 15  <-- Raises FrozenInstanceError

This gives you an immutable object that still behaves like a class (no indexing/unpacking) rather than a tuple. It is often the best middle ground for modern Python development.

Thank you for reading!